#!/usr/bin/env python
"""
Verification of trust anchors for grid entities. Reports expiration of hostcertificate
and expiration of CRL updates. 
Performance metric report: version of EuGridPMA bundle

 -c : critical certificate expiration threshold in hours (24 hours default)
 -w : warning certificate expiration threshold in hours (96 hours (4days) default)

(no check whether c values <= w values)
"""
# last change: bug-fix for 1.38 IGTF bundle 

__author__  = "Placi Flury grid@switch.ch"
__copyright__ = "Copyright 2010, SMSCG an AAA/SWITCH project"
__date__ = "24.08.2010"
__version__ = "0.1.4"

import commands 
import sys, time, calendar
from M2Crypto import X509, m2   # python-m2crypto package >= 0.17 
import os.path 
from nagios_plugin import NagiosPluginBasic, RETURN_CODE

HOSTCERT = "/etc/grid-security/hostcert.pem"
CA_PATH = "/etc/grid-security/certificates"
#CA_SUFFIX = ".0"  # suffix of files in CA_PATH that contain the CA certificate
CA_SUFFIX = ".pem"  # suffix of files in CA_PATH that contain the CA certificate
CRL_SUFFIX = ".r0"  # suffix of CRL files in CA_PATH that contain the CRLs
NO_CRL_EXCEPTIONS = ["304cf809"]  # Certificates that do not have (yet) CRLs

class TrustAnchor(NagiosPluginBasic):
    """
    Checks IGTF trust anchors.
    """
    def __init__(self):
        NagiosPluginBasic.__init__(self)
        self.version = 0.0
        parser = NagiosPluginBasic.getOptions(self)
        parser.remove_option("-H")   # remove hostname option
        parser.add_option("-C", "--crl_critical", action="store",
                            dest="crl_critical_thresh", type="float",
                            default="2.0", help="Critial threshold in hours before CRL expiration. [default= %default]")

        parser.add_option("-W", "--crl_warning", action="store",
                            dest="crl_warning_thresh", type="float",
                            default="12.0", help="Warning threshold in hours before CRL expiration. [default= %default]")
        self.options = parser.parse_args()[0]
        
        if not self.options.critical_thresh:
            self.options.critical_thresh = 24 # 24 hours 
        if not self.options.warn_thresh:
            self.options.warn_thresh = 96 # 96 hours = 4 days 
        
    
        self.options.critical_thresh = float(self.options.critical_thresh)  # still net to cast them ;-(
        self.options.warn_thresh = float(self.options.warn_thresh) 
        self.options.crl_critical_thresh = float(self.options.crl_critical_thresh) 
        self.options.crl_warning_thresh = float(self.options.crl_warning_thresh) 
    
    def check_hostcert(self):
        # check whether both exist
        if not os.path.exists(HOSTCERT) or not os.path.isfile(HOSTCERT):
            print "CRITICAL: hostcertificate file '%s' does not exist." % (HOSTCERT)
            sys.exit(RETURN_CODE["critical"])
        
        if not os.path.exists(CA_PATH):
            print "CRITICAL: CA_PATH '%s' does not exist." % CA_PATH
            sys.exit(RETURN_CODE["critical"])
        
        # check whether can read hostcert
        if not os.access(HOSTCERT, os.R_OK):
            print "CRITICAL: Failed while reading host certificate '%s'. Permission denied." % HOSTCERT
            sys.exit(RETURN_CODE["critical"])

        # check whether hostcert is trusted
        cmd = "openssl verify -CApath %s %s " % (CA_PATH, HOSTCERT)
        output = commands.getoutput(cmd)
        
        if output.find("hostcert.pem: OK") > -1: 
            pass                        # certificate is valid against the trusted CAs
        elif output.find("certificate has expired") > -1:
            print "CRITICAL: hostcert has expired"
            sys.exit(RETURN_CODE["critical"])
        else:
            print "UNKNOWN: unknown error while validating hostcert"
            sys.exit(RETURN_CODE["unknown"])
        
        # check expiration time
        try:
            x509 = X509.load_cert(HOSTCERT, X509.FORMAT_PEM)
            enddate = x509.get_not_after().__str__()
        except Exception, e:
            print  'UNKNOWN: %r' % e
            sys.exit(RETURN_CODE["unknown"])
    
        enddate_tuple = time.strptime(enddate, '%b %d %H:%M:%S %Y GMT')
        enddate_epoch = calendar.timegm(enddate_tuple)
        secs2exp = enddate_epoch - time.mktime(time.localtime())
        hours2exp = secs2exp/3600.0

        if self.options.critical_thresh > hours2exp:
            print "CRITICAL: hostcert expires in %d hours (%0.2f days)" % (hours2exp, hours2exp/24)
            sys.exit(RETURN_CODE["critical"])
        elif (self.options.warn_thresh > hours2exp):
            print "WARNING: hostcert expires in %d hours (%0.2f days)" % (hours2exp, hours2exp/24)
            sys.exit(RETURN_CODE["warning"])
        
            
    def check_crl(self):
        fnames = os.listdir(CA_PATH)
        if len(fnames) == 0:
            print "WARNING: no CAs in '%s' defined" % (CA_PATH)
            sys.exit(RETURN_CODE["warning"])
        

        for file in fnames:
            if file[:-len(CA_SUFFIX)] in NO_CRL_EXCEPTIONS:
                continue
 
            if file[-len(CA_SUFFIX):] == CA_SUFFIX:                                        
                abs_file = os.path.join(CA_PATH, file)
                try:
                    x509 = X509.load_cert(abs_file, X509.FORMAT_PEM)
                    # getting rid of 0x and 'L' affix of hex conversion
                    hash  = hex(x509.get_subject().as_hash())[2:].strip('L')  
                    if len(hash) < 8:  # padding
                        hash = '0' + hash
                    subject = x509.get_subject().as_text(m2.XN_FLAG_RFC2253)
                except Exception, e:
                    print  'UNKNOWN: %r' % e
                    sys.exit(RETURN_CODE["unknown"])
                 

                crl_file = os.path.join(CA_PATH, hash + CRL_SUFFIX)
                if not os.path.isfile(crl_file):
                    print "CRITICAL: CRL file '%s' for '%s' does not exist." % (crl_file, subject)
                    sys.exit(RETURN_CODE["critical"])
                
                try:
                    crl = X509.load_crl(crl_file)
                    crl_text = crl.as_text()
                    next_update_ln = crl_text.split('\n')[5] # !! HACK !!!
                except Exception, e:    
                    print "UNKNOWN: while getting CRL update time for file '%s'." % crl_file
                    sys.exit(RETURN_CODE["unknown"])

                 
                nextupdate = next_update_ln.split('Next Update:')[1].strip()
                nextupdate_tuple = time.strptime(nextupdate, "%b %d %H:%M:%S %Y GMT")
                update_epoch  = time.mktime(nextupdate_tuple)
                secs2update = update_epoch - time.mktime(time.localtime())
                hours2update = secs2update/3600.0

                if hours2update < 0:
                    print "CRITICAL: CRL for %s has expired" % (subject)
                    sys.exit(RETURN_CODE["critical"])

                if self.options.crl_critical_thresh > hours2update:
                    print "CRITICAL: CRL for %s expires in %0.2f hours" % (subject, hours2update)
                    sys.exit(RETURN_CODE["critical"])
                elif self.options.crl_warning_thresh > hours2update:
                    print "WARNING: CRL for %s  expires in %02.f hours" % (subject, hours2update)
                    sys.exit(RETURN_CODE["warning"])
                
                # get version of CA bundle (we look for 'smallest' number)
                info_file = os.path.join(CA_PATH, file[:-len(CA_SUFFIX)] + ".info")
                if os.path.isfile(info_file):
                    try:     
                        infofd = open(info_file,'r')
                        for line in infofd.readlines()[:-1]:
                            if line.find("version") > -1:
                                num = float(line.split("=")[1])
                        if self.version > num or self.version <= 0:
                            self.version = num
                    except:
                        pass
                    infofd.close()

    def getCABundleVersion(self):
        return self.version

    def main(self):
        self.check_hostcert()
        self.check_crl()



if __name__ == "__main__":
    ta = TrustAnchor()
    ta.main()
    print "OK: valid hostcert and CRLs. CA Bundle (v. %0.2f) |CABundleVersion=%0.2f" \
            % (ta.getCABundleVersion(), ta.getCABundleVersion())
    sys.exit(RETURN_CODE["ok"])


